﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>class Singleton { }</example>
    [ExportCodeRefactoringProvider("Singleton", LanguageNames.CSharp)]
    internal class SingletonCodeRefactoringProvider : ICodeRefactoringProvider
    {
        public async Task<IEnumerable<CodeAction>> GetRefactoringsAsync(Document document, TextSpan span, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            if ((var type = root.FindToken(span.Start).Parent as ClassDeclarationSyntax) != null)
            {
                var model = await document.GetSemanticModelAsync(cancellationToken);

                // check whether if it is a non-static, non-abstract class
                INamedTypeSymbol symbol = model.GetDeclaredSymbol(type, cancellationToken);
                if (symbol == null || symbol.IsStatic || symbol.IsAbstract)
                    return null;

                // check whether a static property named "Instance" already exists
                if (symbol.GetMembers("Instance").OfType<PropertyDeclarationSyntax>().Any(p => p.Modifiers.Any(SyntaxKind.StaticKeyword)))
                    return null;

                // check whether it contains only private constructors or the default one
                var constructors = symbol.GetMembers(".ctor").Cast<IMethodSymbol>().ToList();
                if (
                    constructors.All(m => m.DeclaredAccessibility == Accessibility.Private) ||
                    (constructors.Count == 1 && constructors.Single().IsImplicitlyDeclared)
                )
                    return new[] {
                        CodeAction.Create("Generate singleton property (initial value)", c => GenerateWithInitialValueAsync(root, document, type, symbol, c)),
                        CodeAction.Create("Generate singleton property (lazy)", c => GenerateLazyAsync(root, document, model, type, symbol, c)),
                        CodeAction.Create("Generate singleton property (lazy using Lazy)", c => GenerateWithLazyAsync(root, document, model, type, symbol, c)),
                        CodeAction.Create("Generate singleton property (lazy with lock)", c => GenerateWithLockAsync(root, document, model, type, symbol, c)),
                        CodeAction.Create("Generate singleton property (lazy with Interlocked.CompareExchange)", c => GenerateWithCompareExchangeAsync(root, document, model, type, symbol, c)),
                    };
            }

            return null;
        }

        public async Task<Document> GenerateWithInitialValueAsync(SyntaxNode root, Document document, ClassDeclarationSyntax type, INamedTypeSymbol symbol, CancellationToken c)
        {
            ClassDeclarationSyntax withProperty = type;

            // add sealed modifier
            if (!withProperty.Modifiers.Any(SyntaxKind.SealedKeyword))
                withProperty = withProperty
                    .AddModifiers(
                        SyntaxFactory.Token(SyntaxKind.SealedKeyword)
                            .WithTrailingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "))
                    );

            // add private constructor if only implicitly declared exists
            if (symbol.GetMembers(".ctor").Cast<IMethodSymbol>().All(m => m.IsImplicitlyDeclared))
            {
                withProperty = CodeGenerator.AddMethodDeclaration(
                    withProperty,
                    CodeGenerationSymbolFactory.CreateConstructorSymbol(
                        null,
                        Accessibility.Private,
                        default(SymbolModifiers),
                        symbol.Name,
                        null
                    ),
                    document.Project.Solution.Workspace
                );
            }

            // add property
            const string desiredName = "Instance";
            string propertyName = IntroducerHelper.GetNextName(symbol, desiredName);

            withProperty = CodeGenerator.AddPropertyDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreatePropertySymbol(
                    null,
                    Accessibility.Public,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    null,
                    propertyName,
                    null,
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Public,
                        new SymbolModifiers(isAbstract: true),
                        symbol,
                        null,
                        String.Format("get_{0}", propertyName),
                        null,
                        null
                    ),
                    null
                ),
                document.Project.Solution.Workspace
            );

            // add initializer to property
            PropertyDeclarationSyntax property = withProperty.Members.OfType<PropertyDeclarationSyntax>()
                .First(p => p.Identifier.ValueText == propertyName && p.Modifiers.Any(SyntaxKind.StaticKeyword) && p.Modifiers.Any(SyntaxKind.PublicKeyword))
            ;

            withProperty = withProperty.ReplaceNode(
                property,
                property
                    .WithInitializer(
                        SyntaxFactory.EqualsValueClause(
                            SyntaxFactory.ObjectCreationExpression(
                                SyntaxFactory.ParseTypeName(symbol.Name),
                                SyntaxFactory.ArgumentList(),
                                null
                            )
                        )
                    )
                    .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
                    .WithAdditionalAnnotations(Simplifier.Annotation)
                    .WithAdditionalAnnotations(Formatter.Annotation)
            );

            var newRoot = root.ReplaceNode(
                type,
                withProperty
            );

            return document.WithSyntaxRoot(newRoot);
        }

        public async Task<Document> GenerateLazyAsync(SyntaxNode root, Document document, SemanticModel model, ClassDeclarationSyntax type, INamedTypeSymbol symbol, CancellationToken c)
        {
            ClassDeclarationSyntax withProperty = type;

            // add sealed modifier
            if (!withProperty.Modifiers.Any(SyntaxKind.SealedKeyword))
                withProperty = withProperty
                    .AddModifiers(
                        SyntaxFactory.Token(SyntaxKind.SealedKeyword)
                            .WithTrailingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "))
                    );

            // add private constructor if only implicitly declared exists
            if (symbol.GetMembers(".ctor").Cast<IMethodSymbol>().All(m => m.IsImplicitlyDeclared))
            {
                withProperty = CodeGenerator.AddMethodDeclaration(
                    withProperty,
                    CodeGenerationSymbolFactory.CreateConstructorSymbol(
                        null,
                        Accessibility.Private,
                        default(SymbolModifiers),
                        symbol.Name,
                        null
                    ),
                    document.Project.Solution.Workspace
                );
            }

            // add instance field
            const string desiredFieldName = "instance";
            string fieldName = IntroducerHelper.GetNextName(symbol, desiredFieldName);

            withProperty = CodeGenerator.AddFieldDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Private,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    fieldName
                ),
                document.Project.Solution.Workspace
            );

            // add singleton property
            const string desiredPropertyName = "Instance";
            string propertyName = IntroducerHelper.GetNextName(symbol, desiredPropertyName);

            withProperty = CodeGenerator.AddPropertyDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreatePropertySymbol(
                    null,
                    Accessibility.Public,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    null,
                    propertyName,
                    null,
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Public,
                        new SymbolModifiers(),
                        symbol,
                        null,
                        String.Format("get_{0}", propertyName),
                        null,
                        null,
                        new List<SyntaxNode>
                        {
                            SyntaxFactory.IfStatement(
                                SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression,
                                    SyntaxFactory.IdentifierName(fieldName),
                                    SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                                ),
                                SyntaxFactory.ExpressionStatement(
                                    SyntaxFactory.BinaryExpression(SyntaxKind.SimpleAssignmentExpression,
                                        SyntaxFactory.IdentifierName(fieldName),
                                        SyntaxFactory.ObjectCreationExpression(
                                            SyntaxFactory.ParseTypeName(symbol.Name),
                                            SyntaxFactory.ArgumentList(),
                                            null
                                        )
                                    )
                                )
                            )
                                .WithTrailingTrivia(
                                    SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine)
                                ),
                            SyntaxFactory.ReturnStatement(
                                SyntaxFactory.IdentifierName(fieldName)
                            )
                                .WithLeadingTrivia(
                                    SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine)
                                )
                        }
                    ),
                    null
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(
                type,
                withProperty
            );

            return document.WithSyntaxRoot(newRoot);
        }

        public async Task<Document> GenerateWithLockAsync(SyntaxNode root, Document document, SemanticModel model, ClassDeclarationSyntax type, INamedTypeSymbol symbol, CancellationToken c)
        {
            ClassDeclarationSyntax withProperty = type;

            // add sealed modifier
            if (!withProperty.Modifiers.Any(SyntaxKind.SealedKeyword))
                withProperty = withProperty
                    .AddModifiers(
                        SyntaxFactory.Token(SyntaxKind.SealedKeyword)
                            .WithTrailingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "))
                    );

            // add private constructor if only implicitly declared exists
            if (symbol.GetMembers(".ctor").Cast<IMethodSymbol>().All(m => m.IsImplicitlyDeclared))
            {
                withProperty = CodeGenerator.AddMethodDeclaration(
                    withProperty,
                    CodeGenerationSymbolFactory.CreateConstructorSymbol(
                        null,
                        Accessibility.Private,
                        default(SymbolModifiers),
                        symbol.Name,
                        null
                    ),
                    document.Project.Solution.Workspace
                );
            }

            // add sync root
            const string desiredLockName = "syncLock";
            string lockName = IntroducerHelper.GetNextName(symbol, desiredLockName);

            withProperty = CodeGenerator.AddFieldDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Private,
                    new SymbolModifiers(isStatic: true),
                    model.Compilation.GetSpecialType(SpecialType.System_Object),
                    lockName
                ),
                document.Project.Solution.Workspace
            );

            // add initializer
            FieldDeclarationSyntax field = withProperty.Members.OfType<FieldDeclarationSyntax>()
                .First(p => p.Declaration.Variables.FirstOrDefault().Identifier.ValueText == lockName && p.Modifiers.Any(SyntaxKind.StaticKeyword) && p.Modifiers.Any(SyntaxKind.PrivateKeyword))
            ;
            VariableDeclaratorSyntax declaration = field.Declaration.Variables.First();
            FieldDeclarationSyntax fieldWithInitializer = field.ReplaceNode(
                declaration,
                declaration
                    .WithInitializer(
                        SyntaxFactory.EqualsValueClause(
                            SyntaxFactory.ObjectCreationExpression(
                                SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)),
                                SyntaxFactory.ArgumentList(),
                                null
                            )
                        )
                    )
            );
            withProperty = withProperty.ReplaceNode(
                field,
                fieldWithInitializer
            );

            // add instance field
            const string desiredFieldName = "instance";
            string fieldName = IntroducerHelper.GetNextName(symbol, desiredFieldName);

            withProperty = CodeGenerator.AddFieldDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Private,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    fieldName
                ),
                document.Project.Solution.Workspace
            );

            // add volatile modifier
            field = withProperty.Members.OfType<FieldDeclarationSyntax>()
                .First(p => p.Declaration.Variables.FirstOrDefault().Identifier.ValueText == "instance" && p.Modifiers.Any(SyntaxKind.StaticKeyword) && p.Modifiers.Any(SyntaxKind.PrivateKeyword))
            ;
            withProperty = withProperty.ReplaceNode(
                field,
                field
                    .AddModifiers(SyntaxFactory.Token(SyntaxKind.VolatileKeyword))
            );

            // add singleton property
            const string desiredPropertyName = "Instance";
            string propertyName = IntroducerHelper.GetNextName(symbol, desiredPropertyName);

            withProperty = CodeGenerator.AddPropertyDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreatePropertySymbol(
                    null,
                    Accessibility.Public,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    null,
                    propertyName,
                    null,
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Public,
                        new SymbolModifiers(),
                        symbol,
                        null,
                        String.Format("get_{0}", propertyName),
                        null,
                        null,
                        new List<SyntaxNode>
                        {
                            SyntaxFactory.IfStatement(
                                SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression,
                                    SyntaxFactory.IdentifierName(fieldName),
                                    SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                                ),
                                SyntaxFactory.Block(
                                    new StatementSyntax[] {
                                        SyntaxFactory.LockStatement(
                                            SyntaxFactory.IdentifierName(lockName),
                                            SyntaxFactory.Block(
                                                new StatementSyntax[] {
                                                    SyntaxFactory.IfStatement(
                                                        SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression,
                                                            SyntaxFactory.IdentifierName(fieldName),
                                                            SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                                                        ),
                                                        SyntaxFactory.ExpressionStatement(
                                                            SyntaxFactory.BinaryExpression(SyntaxKind.SimpleAssignmentExpression,
                                                                SyntaxFactory.IdentifierName(fieldName),
                                                                SyntaxFactory.ObjectCreationExpression(
                                                                    SyntaxFactory.ParseTypeName(symbol.Name),
                                                                    SyntaxFactory.ArgumentList(),
                                                                    null
                                                                )
                                                            )
                                                        )
                                                    )
                                                }
                                            )
                                        )
                                    }
                                )
                            ),
                            SyntaxFactory.ReturnStatement(
                                SyntaxFactory.IdentifierName(fieldName)
                            )
                        }
                    ),
                    null
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(
                type,
                withProperty
            );

            return document.WithSyntaxRoot(newRoot);
        }

        public async Task<Document> GenerateWithCompareExchangeAsync(SyntaxNode root, Document document, SemanticModel model, ClassDeclarationSyntax type, INamedTypeSymbol symbol, CancellationToken c)
        {
            ClassDeclarationSyntax withProperty = type;

            // add sealed modifier
            if (!withProperty.Modifiers.Any(SyntaxKind.SealedKeyword))
                withProperty = withProperty
                    .AddModifiers(
                        SyntaxFactory.Token(SyntaxKind.SealedKeyword)
                            .WithTrailingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "))
                    );

            // add private constructor if only implicitly declared exists
            if (symbol.GetMembers(".ctor").Cast<IMethodSymbol>().All(m => m.IsImplicitlyDeclared))
            {
                withProperty = CodeGenerator.AddMethodDeclaration(
                    withProperty,
                    CodeGenerationSymbolFactory.CreateConstructorSymbol(
                        null,
                        Accessibility.Private,
                        default(SymbolModifiers),
                        symbol.Name,
                        null
                    ),
                    document.Project.Solution.Workspace
                );
            }

            // add instance field
            const string desiredFieldName = "instance";
            string fieldName = IntroducerHelper.GetNextName(symbol, desiredFieldName);

            withProperty = CodeGenerator.AddFieldDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Private,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    fieldName
                ),
                document.Project.Solution.Workspace
            );

            // add singleton property
            const string desiredPropertyName = "Instance";
            string propertyName = IntroducerHelper.GetNextName(symbol, desiredPropertyName);

            withProperty = CodeGenerator.AddPropertyDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreatePropertySymbol(
                    null,
                    Accessibility.Public,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    null,
                    propertyName,
                    null,
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Public,
                        new SymbolModifiers(),
                        symbol,
                        null,
                        String.Format("get_{0}", propertyName),
                        null,
                        null,
                        new List<SyntaxNode>
                        {
                            SyntaxFactory.IfStatement(
                                SyntaxFactory.BinaryExpression(SyntaxKind.EqualsExpression,
                                    SyntaxFactory.IdentifierName(fieldName),
                                    SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                                ),
                                SyntaxFactory.ExpressionStatement(
                                    SyntaxFactory.InvocationExpression(
                                        SyntaxFactory.ParseName("System.Threading.Interlocked.CompareExchange")
                                            .WithAdditionalAnnotations(Simplifier.Annotation),
                                        SyntaxFactory.ArgumentList(
                                            SyntaxFactory.SeparatedList(
                                                new ArgumentSyntax[] {
                                                    SyntaxFactory.Argument(null, SyntaxFactory.Token(SyntaxKind.RefKeyword), SyntaxFactory.IdentifierName("instance")),
                                                    SyntaxFactory.Argument(
                                                        SyntaxFactory.ObjectCreationExpression( // TODO: Dispose if disposable
                                                            SyntaxFactory.ParseTypeName(symbol.Name),
                                                            SyntaxFactory.ArgumentList(),
                                                            null
                                                        )
                                                    ),
                                                    SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression))
                                                }
                                            )
                                        )
                                    )
                                )
                            )
                                .WithTrailingTrivia(
                                    SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine)
                                ),
                            SyntaxFactory.ReturnStatement(
                                SyntaxFactory.IdentifierName(fieldName)
                            )
                                .WithLeadingTrivia(
                                    SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine)
                                )
                        }
                    ),
                    null
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(
                type,
                withProperty
            );

            return document.WithSyntaxRoot(newRoot);
        }

        public async Task<Document> GenerateWithLazyAsync(SyntaxNode root, Document document, SemanticModel model, ClassDeclarationSyntax type, INamedTypeSymbol symbol, CancellationToken c)
        {
            ClassDeclarationSyntax withProperty = type;

            // add sealed modifier
            if (!withProperty.Modifiers.Any(SyntaxKind.SealedKeyword))
                withProperty = withProperty
                    .AddModifiers(
                        SyntaxFactory.Token(SyntaxKind.SealedKeyword)
                            .WithTrailingTrivia(SyntaxFactory.SyntaxTrivia(SyntaxKind.WhitespaceTrivia, " "))
                    );

            // add private constructor if only implicitly declared exists
            if (symbol.GetMembers(".ctor").Cast<IMethodSymbol>().All(m => m.IsImplicitlyDeclared))
            {
                withProperty = CodeGenerator.AddMethodDeclaration(
                    withProperty,
                    CodeGenerationSymbolFactory.CreateConstructorSymbol(
                        null,
                        Accessibility.Private,
                        default(SymbolModifiers),
                        symbol.Name,
                        null
                    ),
                    document.Project.Solution.Workspace
                );
            }

            // add field
            const string desiredFieldName = "instance";
            string fieldName = IntroducerHelper.GetNextName(symbol, desiredFieldName);

            withProperty = CodeGenerator.AddFieldDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreateFieldSymbol(
                    null,
                    Accessibility.Private,
                    new SymbolModifiers(isStatic: true),
                    model.Compilation.GetTypeByMetadataName(typeof(Lazy<>).FullName).Construct(symbol),
                    fieldName
                ),
                document.Project.Solution.Workspace
            );

            // add initializer
            FieldDeclarationSyntax field = withProperty.Members.OfType<FieldDeclarationSyntax>()
                .First(p => p.Declaration.Variables.FirstOrDefault().Identifier.ValueText == fieldName && p.Modifiers.Any(SyntaxKind.StaticKeyword) && p.Modifiers.Any(SyntaxKind.PrivateKeyword))
            ;
            VariableDeclaratorSyntax declaration = field.Declaration.Variables.First();
            FieldDeclarationSyntax fieldWithInitializer = field.ReplaceNode(
                declaration,
                declaration
                    .WithInitializer(
                        SyntaxFactory.EqualsValueClause(
                            SyntaxFactory.ObjectCreationExpression(
                                SyntaxFactory.ParseTypeName(String.Format("System.Lazy<{0}>", symbol.Name))
                                    .WithAdditionalAnnotations(Simplifier.Annotation),
                                SyntaxFactory.ArgumentList(
                                    SyntaxFactory.SeparatedList(
                                        new ArgumentSyntax[]
                                        {
                                            SyntaxFactory.Argument(
                                                SyntaxFactory.ParenthesizedLambdaExpression(
                                                    SyntaxFactory.ParameterList(),
                                                    SyntaxFactory.ObjectCreationExpression(
                                                        SyntaxFactory.ParseTypeName(symbol.Name),
                                                        SyntaxFactory.ArgumentList(),
                                                        null
                                                    )
                                                )
                                            )
                                        }
                                    )
                                ),
                                null
                            )
                        )
                    )
            );
            withProperty = withProperty.ReplaceNode(
                field,
                fieldWithInitializer
            );

            // add singleton property
            const string desiredPropertyName = "Instance";
            string propertyName = IntroducerHelper.GetNextName(symbol, desiredPropertyName);

            withProperty = CodeGenerator.AddPropertyDeclaration(
                withProperty,
                CodeGenerationSymbolFactory.CreatePropertySymbol(
                    null,
                    Accessibility.Public,
                    new SymbolModifiers(isStatic: true),
                    symbol,
                    null,
                    propertyName,
                    null,
                    CodeGenerationSymbolFactory.CreateMethodSymbol(
                        null,
                        Accessibility.Public,
                        new SymbolModifiers(),
                        symbol,
                        null,
                        String.Format("get_{0}", propertyName),
                        null,
                        null,
                        new List<SyntaxNode>
                        {
                            SyntaxFactory.ReturnStatement(
                                SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                    SyntaxFactory.IdentifierName(fieldName),
                                    SyntaxFactory.IdentifierName("Value")
                                )
                            )
                        }
                    ),
                    null
                ),
                document.Project.Solution.Workspace
            );

            var newRoot = root.ReplaceNode(
                type,
                withProperty
            );

            return document.WithSyntaxRoot(newRoot);
        }

    }
}
